package tp_imag.service.impl;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Vector;
import java.util.Collection;
import gnu.trove.TByteArrayList;

import java.util.List;
import org.apache.log4j.Logger;
import org.hibernate.Session;
import org.hibernate.Transaction;
import tp_imag.model.SSSelement;
import tp_imag.service.NewHibernateUtil;
import tp_imag.service.metricSpaceIndex;

public class metricSpaceIndexImp implements metricSpaceIndex,java.io.Serializable {
	
	static Logger logger = Logger.getLogger(Recorrido.class);
        private byte m;
	private double ALPHA;
	private double coef; // ALPHA * M
	private ArrayList<double[]> elementos;
	private ArrayList<double[]> pivotes;
	private ArrayList<TByteArrayList> storeddist;
	
	private static final long serialVersionUID = 1L;
	private final int INVALID=-1;

	/**
	 * Constructor que inicializa los valores utilizados en el metodo SSS
	 * 
	 * @param d
	 *            Constante entre 0 y 1 que determina la seleccion de pivotes
	 * @throws InvalidFactorException
	 *             Se lanza si el parametro pasado alpha no esta entre 0 y 1
	 */
	public metricSpaceIndexImp(double d){

		if ( (d >= 0) && (d <= 1)) {
			this.ALPHA = d;
			this.m = 0;
		}
	}
	
	/**
	 * Cargar los elementos de la lista que se van a indexar
	 */
	
        private void cargarElementos(Collection<double[]> lista) {
		elementos = new ArrayList<double[]>();
		
		Iterator<double[]> it = lista.iterator();
		while (it.hasNext()){
			elementos.add(it.next());
		}
	}	
	
	/**
	 * Este metodo construye un indice de espacios metricos utilizando el metodo
	 * de Spatial Sparse Selection, llamando a la sobrecarga del metodo con una
	 * cantidad de vectores esperados default seteada en 200.
	 * 
	 * @param lista
	 *            Lista de vectores que son la firma de los contornos
	 * @throws Exception 
	 */
	public void buildIndex(Collection<double[]> lista) throws Exception{
		buildIndex(lista, 200);	//Default de 200 elementos
	}

	/**
	 * Este metodo construye un indice de espacios metricos utilizando el metodo
	 * de Spatial Sparse Selection.
	 * 
	 * @param lista
	 *            Lista de vectores que son la firma de los contornos.
	 * @param expectedwords
	 *            parametro opcional para optimizar si se conoce a priori la
	 *            cantidad de vectores que se leeran o un estimado.
	 * @throws Exception 
	 *             Se devuelve esta excepcion si no hay elementos en el archivo
	 */
	public void buildIndex(Collection<double[]> lista, int expectedwords) throws Exception{

		cargarElementos (lista);
		if (elementos.isEmpty()) {
			throw new Exception();
		}
		coef = m * ALPHA;
		pivotes = new ArrayList<double[]>();
		seleccionarPivotes();

		// Ahora almaceno las distancias precomputadas a los pivotes seleccionados
		storeddist = new ArrayList<TByteArrayList>(pivotes.size()); 
		for (double[] p : pivotes) {
			// Pido suficiente espacio para todos los elementos
			TByteArrayList distelems = new TByteArrayList(elementos.size());
			for (double[] elem : elementos)
				distelems.add(CalcularDistancia(p, elem));
			distelems.trimToSize(); // remuevo espacio innecesario se java aloco extra
			storeddist.add(distelems);
		}
		storeddist.trimToSize();// remuevo espacio innecesario se java aloco extra
		logger.debug("Cantidad de pivotes: " + pivotes.size()
				+ ", que es lo mismo que: " + storeddist.size());
	}

	/**
	 * Metodo privado que encapsula la seleccion inicial de pivotes del indice.
	 * El metodo es llamado por buildIndex y se asegura de que haya al menos un
	 * elemento, sino no se puede efectuar la seleccion de pivotes.
	 * 
	 */
	private void seleccionarPivotes() {

		boolean seguir = true;
		Iterator<double[]> ite_p;

		pivotes.add(elementos.get(0));
		ite_p = pivotes.iterator();
		for (double[] elem : elementos) {
			while ((seguir) && (ite_p.hasNext())) {
				if (CalcularDistancia(ite_p.next(), elem, coef) != INVALID) {
					seguir = false;
				}
			}
			if (seguir) { // implica que segun el criterio debo tomarlo como nuevo pivote
				pivotes.add(elem);
			} else
				seguir = true;
			ite_p = pivotes.iterator(); // vuelvo a pedir iterador para volver a empezar
		}
		pivotes.trimToSize(); // Remuevo espacio innecesario si java aloco extra
	}

	/**
	 * Dadas las ventajas del metodo de SSS, este metodo permite agregar un
	 * elemento al lexico del indice en forma dinamica luego de haberlo creado.
	 * 
	 * @param newelem
	 *            Nuevo elemento a agregar al lexico del indice.
	 */
	public void addElement(double[] newelem) {

		boolean seguir = true;
		Iterator<double[]> ite_p = pivotes.iterator();

		// Primero debo ver si entra como pivote
		while ((seguir) && (ite_p.hasNext())) {
			if (CalcularDistancia(ite_p.next(), newelem, coef) != INVALID) {
				seguir = false;
			}
		}
		if (seguir) { // implica que debo tomarlo como nuevo pivote y guardar las distancias
			pivotes.add(newelem);
			pivotes.trimToSize(); // Remuevo espacio innecesario si java aloco extra
			TByteArrayList distelems = new TByteArrayList(elementos.size());
			for (double[] elem : elementos) {
				distelems.add(CalcularDistancia(newelem, elem));
			}
			distelems.trimToSize();
			storeddist.add(distelems);
			storeddist.trimToSize();
		} // Ahora se agregan los datos del nuevo elemento a la base, sea o no pivote
		elementos.add(newelem); // agrego el elemento a la base
		elementos.trimToSize();
		ite_p = pivotes.iterator();
		for (TByteArrayList dists : storeddist) { // Nota: hay tantos TByteArrayList como pivotes
			dists.add(CalcularDistancia(newelem, ite_p.next()));
			dists.trimToSize();
		}
	}

	public void rangeSearch(double[] query, byte range, ArrayList<double[]> solutions) {

		boolean seguir = true;
		byte dist_elem_piv = -2;
		int i = 0, j, cantpivs = pivotes.size();
		byte dq, distq[] = new byte[cantpivs];

		for (j = 0; j < cantpivs; j++) {
			distq[j] = CalcularDistancia(pivotes.get(j), query);
		}
		for (double[] elem : elementos) {
			j = 0;
			while ((seguir) && (j < cantpivs)) {
				dq = distq[j];
				dist_elem_piv = storeddist.get(j).getQuick(i);
				if ((dist_elem_piv < dq - range) || (dist_elem_piv > dq + range)) {
					seguir = false;
				}
				j++;
			}
			if (seguir) { // implica que el elemento es candidato a solucion
				if (CalcularDistancia(elem, query, range) != INVALID) {
					//solutions.add(new String(elem));
					solutions.add(elem);
				}
			} else {
				seguir = true;
			}
			i++;
		}
	}

	public void rangeSearch2(double[] query, byte range, ArrayList<Integer> solutions) {

                verificarDatos();

		boolean seguir = true;
		byte dist_elem_piv = -2;
		int i = 0, j, cantpivs = pivotes.size();
                int numElem = 0;
		byte dq, distq[] = new byte[cantpivs];

		for (j = 0; j < cantpivs; j++) {
			distq[j] = CalcularDistancia(pivotes.get(j), query);
		}
		for (double[] elem : elementos) {
			j = 0;
			while ((seguir) && (j < cantpivs)) {
				dq = distq[j];
				dist_elem_piv = storeddist.get(j).getQuick(i);
				if ((dist_elem_piv < dq - range) || (dist_elem_piv > dq + range)) {
					seguir = false;
				}
				j++;
			}
			if (seguir) { // implica que el elemento es candidato a solucion
				if (CalcularDistancia(elem, query, range) != INVALID) {
					//solutions.add(new String(elem));
					solutions.add(numElem + 1);
				}
			} else {
				seguir = true;
			}
			i++;
                        numElem++;
		}
	}

        private void verificarDatos(){
            ArrayList<double[]> lista = new ArrayList<double[]>();
            //if (elementos == null || elementos.isEmpty()){
            elementos=null;
            pivotes = null;
                try {
                    Session session = NewHibernateUtil.getSessionFactory().getCurrentSession();
                    Transaction tx = session.beginTransaction();

                    List elementosGuardados = session.createQuery("select e from SSSelement as e order by id asc").list();
                    for (Iterator iter = elementosGuardados.iterator(); iter.hasNext();) {
                        SSSelement sssElement = (SSSelement) iter.next();
                        lista.add(sssElement.getValores());
                    }
                    tx.commit();
                } catch (RuntimeException e) {
                    // throw again the first exception
                    throw e;
                }
                //cargarElementos(lista);
                try{
                    buildIndex(lista);
                }
                catch (Exception e){
                    e.printStackTrace();
                }
            //}else{
            //   logger.info("No es necesario armar INDICE");
            //}
        }


	public void kNNSearch(String query, int k, ArrayList<String> solutions) {

		// TODO: Implementar ???
	}	
	
	public void save (String fileName) throws IOException{
		ObjectOutputStream obj_out = new ObjectOutputStream(new FileOutputStream(fileName));
		obj_out.writeObject(this);
		obj_out.close();
	}
	
	public static metricSpaceIndex load (String fileName) throws IOException, ClassNotFoundException{
		ObjectInputStream obj_in = new ObjectInputStream (new FileInputStream (fileName));
		metricSpaceIndex index= (metricSpaceIndex) obj_in.readObject();
		obj_in.close();
		return index;
	}	
	
	public double[] CalcularModulo(Vector<CoordenadaTransformada> vt) {
		
		double vm[] = new double[300];
		
		double mod,norma=0;
		//Calculo el modulo de cada coordenada
		for(int i=0;i<300;i++){
			CoordenadaTransformada punto =vt.get(i);
			mod=Math.pow(punto.getX(),2)+Math.pow(punto.getY(),2);
			mod=Math.sqrt(mod);
			
			vm[i]=mod;
			norma+=Math.pow(mod,2);
		}		
		//Normalizo el vector y lo dejo para que entre un byte
		mod=0;
		for(int i=0;i<300;i++){
			vm[i]=(vm[i]/Math.sqrt(norma))*100;
		}		
		
		return vm;
	}
	
	private byte CalcularDistancia(double[] v1,double[] v2){
		
		double d=0;
		for(int i=0;i<300;i++){
			d+=Math.pow(v1[i]-v2[i],2);
		}
		logger.debug("Distancia entre 1 y 2: " + Math.sqrt(d) + " - " + (byte)Math.sqrt(d));
		return (byte)Math.sqrt(d);
	}
	
	private byte CalcularDistancia(double[] v1,double[] v2,double coef){
		double d=CalcularDistancia(v1,v2);
		
		if(d<=coef)
			return (byte)d;
		else
			return -1;
	}	
}
